// 实现日志

// 防止头文件被重复包含
#ifndef __LOG__HPP
#define __LOG__HPP

#include <iostream>
#include <string>
#include <cstdio>
#include <filesystem> // C++文件操作 -- C++17
#include <fstream>    // C++文件操作
#include <sstream>    // 流式格式控制
#include <sys/types.h>
#include <unistd.h>
#include <memory>
#include <time.h>
#include "Mutex.hpp"

namespace LogModel
{
    // 分隔符 \r\n
    const std::string gsep = "\r\n";

    using namespace MutexModel;

    // 先实现日志的刷新策略

    // 日志策略 --- 策略模式 !
    // 实现刷新策略 , 我们用继承实现 ! 多态可以执行不同的策略
    // 所以继承多态可以实现基类一样 , 子类不一样执行不同任务
    // 所以 , 策略模式就是C++继承多态特性

    // 刷新策略 : 1. 显示器刷新 2. 向指定文件刷新
    // 基类
    class LogStrategy
    {
    public:
        // 使用默认
        LogStrategy() = default;
        ~LogStrategy() = default;

        // 虚函数 , 因为多态实现必须对虚函数重写
        // 这是一个纯虚函数 , 只用声明不用实现 , 因为要被派生类重写

        // 刷新 , 所有传刷新内容
        virtual void SyncLog(const std::string &message) = 0;
    };

    ///////////////   继承 --- 两种策略
    // 1. 显示器打印日志策略 -- 子类(派生类)
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy() {}
        ~ConsoleLogStrategy() {}

        // 对虚函数重写 , 子类一定要对虚函数重写才能实现多态 !
        // 重写三同 : 返回值 , 参数列表 , 函数名, 只要里面的实现不同
        // 重写基类虚函数
        // 把打印的信息传过来
        void SyncLog(const std::string &message) override
        {
            // 这里要加锁 , 防止打印混乱
            LockGuard lockguard(_mutex);
            std::cout << message << gsep; // ... \r\n
        }

    private:
        Mutex _mutex;
    };

    // 2. 向文件刷新策略 , 子类
    // 既然是文件 , 就要有文件的路径和文件名吧

    // 这里定义一下默认的日志形成的路径和日志文件名
    std::string default_path = "./log";
    std::string default_file = "text.log";
    // 文件这里用 C++的文件操作
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(std::string &path = default_path,
                        std::string &filename = default_file)
            : _path(path), _file(filename)
        {
            {
                LockGuard lockguard(_mutex);
                // 构造时 , 构造出来路径
                // std::filesystem::exists(path) 判断一个路径是否存在
                // 存在了 , 返回证明路径合法
                if (std::filesystem::exists(_path))
                {
                    return;
                }

                // 路径不存在 , 创建路径
                // 创建失败 , 抛出异常--文件错误异常
                try
                {
                    // 创建路径
                    std::filesystem::create_directories(_path);
                }
                catch (std::filesystem::filesystem_error &e)
                {
                    std::cerr << e.what() << std::endl;
                }
            }
        }

        ~FileLogStrategy() {}

        // 重写虚函数 , override 检测是否被重写
        void SyncLog(const std::string &message) override
        {
            {
                LockGuard lockguard(_mutex);
                // 文件操作

                // 1. 打开文件 ,但是得有一个日志文件的总路径吧 !
                // 路径就为 : 日志文件所在路径 + 日志名
                // 拼接总路径名 , ./xx/xxx/xx  ,_path默认 "./"
                _pathname = _path + (_path.back() == '/' ? "" : "/") + _file;
                //** 打开文件  */ -- 追加方式打开 , 使用C++文件操作
                // ofstream 使用对象打开打开文件
                std::ofstream out(_pathname, std::ios::app); // 追加方式打开文件
                if (!out.is_open())
                {
                    return; // 打开失败返回
                }

                // 打开成功 , 写文件 , 向文件流中写 , 即 : 向打开的文件中写
                out << message << gsep;

                out.close();
            }
        }

    private:
        // string 在这里不能赋值 , 否则抛出异常 !!!!
        std::string _path;     // 日志文件所在路径
        std::string _file;     // 日志文件本身
        std::string _pathname; // 总路径名

        Mutex _mutex; // 加锁 , 不能都写
    };

    // ********************  至此以上完成刷新策略 ********************

    // 第二步 : 生成一个完整的日志
    // 1 . 一个完整的日志最少包含 : 1. 日志等级 2.时间 3. 进程pid 4. 文件名 5.行号 6. 内容
    // 2 . 刷新策略 , 选择刷新策略

    // 1 . 形成日志等级 - 日志等级用枚举
    // 枚举类型都是整数
    enum class LogLevel
    {
        DEBUG,   // 调试
        INFO,    // 正常
        WARNING, // 警告
        ERROR,   // 错误
        FATAL    // 致命
    };

    // 2. 枚举类型转成字符串 -- 日志等级转字符串
    std::string LevelToString(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return nullptr;
        }
    }

    // 自动获取 : 时间戳
    std::string GetTimeStamp()
    {
// 这里用 localtime_r  , 这个函数是用来获取时间戳的
// struct tm *localtime_r(const time_t *timep, struct tm *result);
// 第一个参数是 time_t 类型的 , time 系统调用就是返回这个类型的
#if 0
        // 返回的是一个结构体 , 结构体就是一下这个 : 
        //   struct tm {
        //        int tm_sec;    /* Seconds (0-60) */
        //        int tm_min;    /* Minutes (0-59) */
        //        int tm_hour;   /* Hours (0-23) */
        //        int tm_mday;   /* Day of the month (1-31) */
        //        int tm_mon;    /* Month (0-11) */
        //        int tm_year;   /* Year - 1900 */ ******* 注意这里的年是 -1900的 ,所以我们使用要加上1900
        //        int tm_wday;   /* Day of the week (0-6, Sunday = 0) */
        //        int tm_yday;   /* Day in the year (0-365, 1 Jan = 0) */
        //        int tm_isdst;  /* Daylight saving time */
        //    };
#endif

        // 1. 用 time 系统调用获取 time_t 类型
        time_t curr = time(nullptr);
        // 2. 创建 struct tm 一个结构体 , 是一个输出型参数
        struct tm curr_tm;
        // 3. 用 localtime_t 获取时间戳 , 返回的就是 ^|^ 上面的这个结构体
        localtime_r(&curr, &curr_tm);

        // 但是我想要的是 2021-01-03 这样的格式 , 所以格式化到一个字符数组中返回就行了
        char timebuffer[128];
        snprintf(timebuffer, sizeof(timebuffer),
                 "%4d-%02d-%02d %0d:%0d:%0d",
                 curr_tm.tm_year + 1900, // 因为年是 -1900的 , 所以我们要加上
                 curr_tm.tm_mon + 1,     // 因为月是 0-11 , 所以要+1
                 curr_tm.tm_mday,
                 curr_tm.tm_hour,
                 curr_tm.tm_min,
                 curr_tm.tm_sec);
        // 不能用引用 , 因为是临时空间
        return timebuffer;
    }

    class Logger
    {
    public:
        Logger()
        {
            // 默认使用显示器刷新
            EnableConsoleLogStrategy();
        }
        ~Logger() {}

        // 刷新策略
        void EnableFileLogStrategy()
        {
            // 文件策略子类
            //  构造子类对象 , 就会调用子类 , 这样写更简洁
            _fflush_strategy = std::make_unique<FileLogStrategy>();
        }

        void EnableConsoleLogStrategy()
        {
            // 显示器打印子类
            _fflush_strategy = std::make_unique<ConsoleLogStrategy>();
        }

        // 这里包含一个内部类 , 内部类能访问外部类的私有
        // 用于形成一条日志信息 , 这样多线程调用就可以有多条
        class LogMessage
        {
        public:
            LogMessage(LogLevel &level, std::string &filename, int line_num, Logger &logger)
                : _curret_time(GetTimeStamp()) // 获取时间戳
                  ,
                  _log_level(level), _pid(getpid()), _src_file_name(filename), _line_num(line_num), _logger(logger)
            {
                //[][][][][] - xxx(这个是我们写的内容)
                // 最终合并的信息要是这样的 , 所以先合成左边
                // 左边用 C++ 格式控制
                std::stringstream ss; // 会自动识别类型合并
                ss << "[" << _curret_time << "] "
                   << "[" << LevelToString(_log_level) << "] " // 枚举类型默认都是整数所以要转成字符串
                   << "[" << _pid << "] "
                   << "[" << _src_file_name << "] "
                   << "[" << _line_num << "] "
                   << "- ";
                // 以上就把 [][][][][] - 这半部分定义好了 ,但是还有拼接
                _loginfo = ss.str(); // 这样就把我们定义的拼接成一个字符串了
                // 但是完整信息应该是左边形成的 + 我们自己写的内容
                // 我们写可能会这样写 << xx << xx << xx
                // 所以 , 会有多个流操作符 , 那么对于这样我们就要重载支持多个操作符
                // 同时可能还会写多种类型 << "jear" << 123 << "c"
                // 所以我们要写重载函数 , 重载 <<
            }

            // 重载  << -- 模版的
            //  LogMessage() << "hell world" << "XXXX" << 3.14 << 1234
            //  所以我们会这样写 , 把东西写到日志中 , 所以返回日志对象 , 证明还能继续写(继续 <<)
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info; // 把信息写到这里最后和 _loginfo 拼接

                // 以上就把 [][][][][] - xxx
                _loginfo += ss.str(); // 右边内容和左边的拼接

                // 但是还有支持多个 << , 返回什么对象 ??
                return *this; // 返回 LogMessage  this 指针就是 LogMessage , 因为类的成员函数有this指针
            }

            ///////////////// 以上就是完整了日志信息的整合拼接 , 但是还没有刷新

            // 析构时 , 根据刷新策略刷新
            // 但是刷新策略在外部类中 , 所以构造时要传外部类对象 , 这就是用内部类的原因
            // 根据外部类的刷新策略 , 因为外部类中有刷新策略 _fflush_strategy 智能指针
            ~LogMessage()
            {
                // 刷新策略不为空  , 刷新
                if (_logger._fflush_strategy)
                {
                    // 这里就是多态的调用
                    // 多态必须是基类的指针或引用调用 , 这里就是指针
                    // 基类指向哪个子类就执行哪个子类的虚函数
                    //_fflush_strategy 就是一个指针
                    // 刷新一条日志信息 , 传过去拼接好的日志信息
                    _logger._fflush_strategy->SyncLog(_loginfo);
                }
            }

        private:
            // 一条日志包含以下信息 :
            std::string _curret_time;   // 时间 , 要系统自动获取
            LogLevel _log_level;        // 日志等级
            pid_t _pid;                 // 进程的 pid , 用系统获取
            std::string _src_file_name; // 所在源文件名
            int _line_num;              // 行号
            std::string _loginfo;       // 合并之后，一条完整的信息

            // 外部类
            Logger &_logger;                 // 定义外部类
            std::stringstream _manip_stream; // 支持 std::endl
        };

        // 使用日志都是用 Logger 这个类 , 所以我想以仿函数的方式访问 , 就是像函数一样
        //  Logger(xx,xx,xx) , 但是真正的一条日志是 LogMessage类

        //************* 注意 , 这里写的就是返回** 临时对象 **  **************/
        // 因为返回后 , LogMessage 是拷贝 , 其就会自动调用 LogMessage 的析构刷新日志内容
        // 因为临时对象 , 生命周期只在本函数内有效 , 所有生命周期一到调用析构
        LogMessage operator()(LogLevel level, std::string name, int line)
        {
            // 直接调用 LogMessage定义对象

            return LogMessage(level, name, line, *this);
        }

    private:
        // 一个日志要有刷新策略 , 因为策略是多态
        // 所以必须是基类的指针或引用调用才可以
        // 用指针指针 , 作为基类的指针
        // 这样基类指向哪个子类就调用谁
        std::unique_ptr<LogStrategy> _fflush_strategy; // 刷新策略
    };

    // 用户用日志都是用的 Logger
    // 所以 , 这里直接定义一个 Logger 全局对象 , 以下简化

    // 全局日志对象
    Logger logger;

// 1 . 本来是用 logger(level , name  , line) 以仿函数的方式使用
// 2 . 定义出来以后 , 选择刷新策略
//    logger.xxxx

// ************ 以下的均是为了简化操作! ************ -- 用宏

// 自动获取行号和源文件名 , 这个是在编译时自动就会替换
//  文件名 : __FILE__
//  行号 :   __LINE__

// 所以 , 用户使用就可以 : LOG(level) 这样就行了
#define LOG(level) logger(level, __FILE__, __LINE__)                    // 宏函数
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy() // 宏函数
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()       // 宏函数

    // ************ 简化操作! ************

}

#endif
